package daybook.io.shell;

import daybook.io.formatter.DefaultFormatterFactory;
import daybook.io.formatter.FormatterFactory;
import daybook.model.Account;
import daybook.model.Book;
import daybook.model.ChartOfAccounts;
import daybook.model.Commodity;
import daybook.model.transaction.*;
import daybook.util.ImmutablePair;
import daybook.util.Pair;
import daybook.util.StreamUtils;
import daybook.util.tree.Tree;

import javax.script.*;
import java.io.IOException;
import java.io.Writer;
import java.math.BigDecimal;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalUnit;
import java.util.*;
import java.util.concurrent.ConcurrentMap;
import java.util.function.*;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * Created by crc on 16-9-2.
 */
public class ShellContext {
    public final Book book;
    public final List<Transaction> transactions;
    public final ChartOfAccounts chartOfAccounts;
    public final Commodity defaultCurrency;
    public final Map<String, Commodity> commodities;
    public final String accountNameSeparator;
    public final FormatterFactory formatterFactory;

    public ShellContext(Book book, List<Transaction> transactions) {
        this.book = Objects.requireNonNull(book);
        this.transactions = Objects.requireNonNull(transactions);
        this.chartOfAccounts = book.getChartOfAccounts();
        this.defaultCurrency = book.getDefaultCurrency();
        this.commodities = book.getCommodities();
        this.accountNameSeparator = book.getAccountNameSeparator();
        this.formatterFactory = new DefaultFormatterFactory(book);
    }


    public static Writer fileWriter(String path, String charset) throws IOException {
        final Path p = Paths.get(path);
        final Charset cs = Charset.forName(charset != null ? charset : "UTF-8");
        return Files.newBufferedWriter(p, cs);
    }

    public static Path cwd() {
        return Paths.get(".").toAbsolutePath().normalize();
    }

    public static <A, B> Pair<A, B> pair(A a, B b) {
        return ImmutablePair.of(a, b);
    }

    public static DateSet dates(LocalDate startDate, LocalDate endDate, TemporalUnit period) {
        return new DateSet(startDate, endDate, period);
    }

    public static DateSet dates(String startDate, String endDate, TemporalUnit period) {
        return dates(LocalDate.parse(startDate), LocalDate.parse(endDate), period);
    }

    public static Predicate<Transaction> filterByTransactionEntry(Predicate<TransactionEntry> transactionEntryPredicate) {
        return TransactionSummary.transactionPredicateByEntry(transactionEntryPredicate);
    }

    public static Predicate<TransactionEntry> filterByAccount(Predicate<Account> accountPredicate) {
        return TransactionSummary.transactionEntryPredicateByAccount(accountPredicate);
    }

    public static Predicate<TransactionEntry> filterByCommodity(Predicate<Commodity> commodityPredicate) {
        return TransactionSummary.transactionEntryPredicateByCommodity(commodityPredicate);
    }

    public static Function<TransactionEntry, List<VirtualAccount>> classifyByVirtualAccount(VirtualAccount virtualAccount) {
        return TransactionSummary.makeVirtualAccountClassifier(virtualAccount);
    }

    public static Function<TransactionEntry, List<VirtualAccount>> classifyByVirtualAccount(Collection<VirtualAccount> virtualAccounts) {
        return TransactionSummary.makeVirtualAccountClassifier(virtualAccounts);
    }

    public static Function<TransactionEntry, Account> classifyByAccount() {
        return TransactionEntry::getAccount;
    }

    public static BinaryOperator<List<LedgerEntry>> appendLedgerEntries() {
        return TransactionSummary.ledgerEntriesCombiner();
    }

    public static BinaryOperator<List<InventoryEntry>> appendInventoryEntries() {
        return TransactionSummary.inventoryEntriesCombiner();
    }

    public static <T, A, R> Collector<T, ?, R> filter(Predicate<? super T> predicate, Collector<? super T, A, R> downstream) {
        return StreamUtils.filter(predicate, downstream);
    }

    public static <T, K, D, A>
    Collector<T, ?, Map<K, D>> multiGroupingBy(Function<? super T, ? extends Iterable<? extends K>> classifier,
                                               Collector<? super T, A, D> downstream) {
        return StreamUtils.multiGroupingBy(classifier, downstream);
    }

    public static <T, K, D, A, M extends Map<K, D>>
    Collector<T, ?, M> multiGroupingBy(Function<? super T, ? extends Iterable<? extends K>> classifier,
                                       Supplier<M> mapFactory,
                                       Collector<? super T, A, D> downstream) {
        return StreamUtils.multiGroupingBy(classifier, mapFactory, downstream);
    }

    public static <T, U, A, R>
    Collector<T, ?, R> flatMapping(Function<? super T, ? extends Stream<? extends U>> mapper,
                                   Collector<? super U, A, R> downstream) {
        return StreamUtils.flatMapping(mapper, downstream);
    }

    public static <T, C extends Collection<T>>
    Collector<T, ?, C> toCollection(Supplier<C> collectionFactory) {
        return Collectors.toCollection(collectionFactory);
    }

    public static  <T> Collector<T, ?, List<T>> toList() {
        return Collectors.toList();
    }

    public static <T> Collector<T, ?, Set<T>> toSet() {
        return Collectors.toSet();
    }

    public static Collector<CharSequence, ?, String> joining() {
        return Collectors.joining();
    }

    public static Collector<CharSequence, ?, String> joining(CharSequence delimiter) {
        return Collectors.joining(delimiter);
    }

    public static Collector<CharSequence, ?, String> joining(CharSequence delimiter,
                                                             CharSequence prefix,
                                                             CharSequence suffix) {
        return Collectors.joining(delimiter, prefix, suffix);
    }

    public static <T, U, A, R>
    Collector<T, ?, R> mapping(Function<? super T, ? extends U> mapper,
                               Collector<? super U, A, R> downstream) {
        return Collectors.mapping(mapper, downstream);
    }

    public static <T,A,R,RR> Collector<T,A,RR> collectingAndThen(Collector<T,A,R> downstream,
                                                                 Function<R,RR> finisher) {
        return Collectors.collectingAndThen(downstream, finisher);
    }

    public static <T> Collector<T, ?, Long> counting() {
        return Collectors.counting();
    }

    public static <T> Collector<T, ?, Optional<T>>
    minBy(Comparator<? super T> comparator) {
        return Collectors.minBy(comparator);
    }

    public static <T> Collector<T, ?, Optional<T>>
    maxBy(Comparator<? super T> comparator) {
        return Collectors.maxBy(comparator);
    }

    public static <T> Collector<T, ?, Integer>
    summingInt(ToIntFunction<? super T> mapper) {
        return Collectors.summingInt(mapper);
    }

    public static <T> Collector<T, ?, Long>
    summingLong(ToLongFunction<? super T> mapper) {
        return Collectors.summingLong(mapper);
    }

    public static <T> Collector<T, ?, Double>
    summingDouble(ToDoubleFunction<? super T> mapper) {
        return Collectors.summingDouble(mapper);
    }

    public static <T> Collector<T, ?, Double>
    averagingInt(ToIntFunction<? super T> mapper) {
        return Collectors.averagingInt(mapper);
    }

    public static <T> Collector<T, ?, Double>
    averagingLong(ToLongFunction<? super T> mapper) {
        return Collectors.averagingLong(mapper);
    }

    public static <T> Collector<T, ?, Double>
    averagingDouble(ToDoubleFunction<? super T> mapper) {
        return Collectors.averagingDouble(mapper);
    }

    public static <T> Collector<T, ?, T>
    reducing(T identity, BinaryOperator<T> op) {
        return Collectors.reducing(identity, op);
    }

    public static <T> Collector<T, ?, Optional<T>>
    reducing(BinaryOperator<T> op) {
        return Collectors.reducing(op);
    }

    public static <T, U>
    Collector<T, ?, U> reducing(U identity,
                                Function<? super T, ? extends U> mapper,
                                BinaryOperator<U> op) {
        return Collectors.reducing(identity, mapper, op);
    }

    public static <T, K> Collector<T, ?, Map<K, List<T>>>
    groupingBy(Function<? super T, ? extends K> classifier) {
        return Collectors.groupingBy(classifier);
    }

    public static <T, K, A, D>
    Collector<T, ?, Map<K, D>> groupingBy(Function<? super T, ? extends K> classifier,
                                          Collector<? super T, A, D> downstream) {
        return Collectors.groupingBy(classifier, downstream);
    }

    public static <T, K, D, A, M extends Map<K, D>>
    Collector<T, ?, M> groupingBy(Function<? super T, ? extends K> classifier,
                                  Supplier<M> mapFactory,
                                  Collector<? super T, A, D> downstream) {
        return Collectors.groupingBy(classifier, mapFactory, downstream);
    }

    public static <T>
    Collector<T, ?, Map<Boolean, List<T>>> partitioningBy(Predicate<? super T> predicate) {
        return Collectors.partitioningBy(predicate);
    }

    public static <T, D, A>
    Collector<T, ?, Map<Boolean, D>> partitioningBy(Predicate<? super T> predicate,
                                                    Collector<? super T, A, D> downstream) {
        return Collectors.partitioningBy(predicate, downstream);
    }

    public static <T, K, U>
    Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
                                    Function<? super T, ? extends U> valueMapper) {
        return Collectors.toMap(keyMapper, valueMapper);
    }

    public static <T, K, U>
    Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
                                    Function<? super T, ? extends U> valueMapper,
                                    BinaryOperator<U> mergeFunction) {
        return Collectors.toMap(keyMapper, valueMapper, mergeFunction);
    }

    public static <T, K, U, M extends Map<K, U>>
    Collector<T, ?, M> toMap(Function<? super T, ? extends K> keyMapper,
                             Function<? super T, ? extends U> valueMapper,
                             BinaryOperator<U> mergeFunction,
                             Supplier<M> mapSupplier) {
        return Collectors.toMap(keyMapper, valueMapper, mergeFunction, mapSupplier);
    }

    public static <T, K, U>
    Collector<T, ?, ConcurrentMap<K,U>> toConcurrentMap(Function<? super T, ? extends K> keyMapper,
                                                        Function<? super T, ? extends U> valueMapper) {
        return Collectors.toConcurrentMap(keyMapper, valueMapper);
    }

    public static <T, K, U>
    Collector<T, ?, ConcurrentMap<K,U>>
    toConcurrentMap(Function<? super T, ? extends K> keyMapper,
                    Function<? super T, ? extends U> valueMapper,
                    BinaryOperator<U> mergeFunction) {
        return Collectors.toConcurrentMap(keyMapper, valueMapper, mergeFunction);
    }

    public static <T, K, U, M extends ConcurrentMap<K, U>>
    Collector<T, ?, M> toConcurrentMap(Function<? super T, ? extends K> keyMapper,
                                       Function<? super T, ? extends U> valueMapper,
                                       BinaryOperator<U> mergeFunction,
                                       Supplier<M> mapSupplier) {
        return Collectors.toConcurrentMap(keyMapper, valueMapper, mergeFunction, mapSupplier);
    }

    public static <T>
    Collector<T, ?, IntSummaryStatistics> summarizingInt(ToIntFunction<? super T> mapper) {
        return Collectors.summarizingInt(mapper);
    }

    public static <T>
    Collector<T, ?, LongSummaryStatistics> summarizingLong(ToLongFunction<? super T> mapper) {
        return Collectors.summarizingLong(mapper);
    }

    public static <T>
    Collector<T, ?, DoubleSummaryStatistics> summarizingDouble(ToDoubleFunction<? super T> mapper) {
        return Collectors.summarizingDouble(mapper);
    }

    public static <T> Collector<Transaction, ?, NavigableMap<LocalDate, T>> groupingByDate(NavigableSet<LocalDate> dates, Collector<Transaction, ?, T> collector) {
        return TransactionSummary.groupingByDate(dates, collector);
    }

    public static <T, M extends Map<LocalDate, T>> Collector<Transaction, ?, M> groupingByDate(NavigableSet<LocalDate> dates, Supplier<M> mapFactory, Collector<Transaction, ?, T> collector) {
        return TransactionSummary.groupingByDate(dates, mapFactory, collector);
    }

    public static Collector<BigDecimal, ?, BigDecimal> amountCollector() {
        return TransactionSummary.amountCollector();
    }

    public static Collector<Pair<Transaction, TransactionEntry>, List<LedgerEntry>, List<LedgerEntry>> ledgerEntriesCollector() {
        return TransactionSummary.ledgerEntriesCollector();
    }

    public static Collector<Pair<Transaction, TransactionEntry>, List<LedgerEntry>, List<LedgerEntry>> ledgerEntriesCollector(Supplier<List<LedgerEntry>> supplier) {
        return TransactionSummary.ledgerEntriesCollector(supplier);
    }

    public static <T> Collector<Pair<Transaction, TransactionEntry>, Map<T, List<LedgerEntry>>, Map<T, List<LedgerEntry>>> ledgerCollector(Function<TransactionEntry, ? extends T> classifier, Function<T, BigDecimal> initBalance) {
        return TransactionSummary.ledgerCollector(classifier, initBalance);
    }

    public static <T> Collector<Pair<Transaction, TransactionEntry>, Map<T, List<LedgerEntry>>, Map<T, List<LedgerEntry>>> ledgerMultiCollector(Function<TransactionEntry, ? extends Iterable< ? extends T>> classifier, Function<T, BigDecimal> initBalance) {
        return TransactionSummary.ledgerMultiCollector(classifier, initBalance);
    }

    public static Collector<Item, Map<BigDecimal, BigDecimal>, Map<BigDecimal, BigDecimal>> costBalanceCollector(int dc) {
        return TransactionSummary.costBalanceCollector(dc);
    }

    public static Collector<Item, Map<BigDecimal, BigDecimal>, Map<BigDecimal, BigDecimal>> costBalanceCollector(int dc, Supplier<Map<BigDecimal, BigDecimal>> supplier) {
        return TransactionSummary.costBalanceCollector(dc, supplier);
    }

    public static Collector<Pair<Transaction, TransactionEntry>, List<InventoryEntry>, List<InventoryEntry>> inventoryEntriesCollector() {
        return TransactionSummary.inventoryEntriesCollector();
    }

    public static Collector<Pair<Transaction, TransactionEntry>, List<InventoryEntry>, List<InventoryEntry>> inventoryEntriesCollector(Supplier<List<InventoryEntry>> supplier) {
        return TransactionSummary.inventoryEntriesCollector(supplier);
    }

    public static <T> Collector<Pair<Transaction, TransactionEntry>, Map<T, List<InventoryEntry>>, Map<T, List<InventoryEntry>>> inventoryCollector(Function<TransactionEntry, ? extends T> classifier, Function<T, BigDecimal> initBalance, Function<T, BigDecimal> initCountBalance, Function<T, Map<BigDecimal, BigDecimal>> initCostBalance) {
        return TransactionSummary.inventoryCollector(classifier, initBalance, initCountBalance, initCostBalance);
    }

    public static <T> Collector<Pair<Transaction, TransactionEntry>, Map<T, List<InventoryEntry>>, Map<T, List<InventoryEntry>>> inventoryMultiCollector(Function<TransactionEntry, ? extends Iterable<? extends T>> classifier, Function<T, BigDecimal> initBalance, Function<T, BigDecimal> initCountBalance, Function<T, Map<BigDecimal, BigDecimal>> initCostBalance) {
        return TransactionSummary.inventoryMultiCollector(classifier, initBalance, initCountBalance, initCostBalance);
    }

    public static Collector<TransactionEntry, BigDecimal[], BigDecimal[]> flowCollector() {
        return TransactionSummary.flowCollector();
    }

    public static Collector<BigDecimal[], BigDecimal[], BigDecimal[]> amountsCollector() {
        return TransactionSummary.amountsCollector();
    }

    public static Map<LocalDate, Map<VirtualAccount, BigDecimal>> balance(Stream<Transaction> transactions,
                                                                          NavigableSet<LocalDate> dates,
                                                                          Set<VirtualAccount> virtualAccounts) {
        return TransactionSummary.balance(transactions, dates, virtualAccounts);
    }

    public static Map<LocalDate, Map<VirtualAccount, BigDecimal>> balance(List<Transaction> transactions,
                                                                          NavigableSet<LocalDate> dates,
                                                                          Set<VirtualAccount> virtualAccounts) {
        return TransactionSummary.balance(transactions, dates, virtualAccounts);
    }

    public static NavigableMap<LocalDate, Map<Account, BigDecimal>> balance(Stream<Transaction> transactions,
                                                                            NavigableSet<LocalDate> dates) {
        return TransactionSummary.balance(transactions, dates);
    }

    public static NavigableMap<LocalDate, Map<Account, BigDecimal>> balance(List<Transaction> transactions,
                                                                            NavigableSet<LocalDate> dates) {
        return TransactionSummary.balance(transactions, dates);
    }

    public static NavigableMap<LocalDate, Map<Account, BigDecimal>> balance(Stream<Transaction> transactions,
                                                                            NavigableSet<LocalDate> dates,
                                                                            Predicate<Account> accountPredicate) {
        return TransactionSummary.balance(transactions, dates, accountPredicate);
    }

    public static NavigableMap<LocalDate, Map<Account, BigDecimal>> balance(List<Transaction> transactions,
                                                                            NavigableSet<LocalDate> dates,
                                                                            Predicate<Account> accountPredicate) {
        return TransactionSummary.balance(transactions, dates, accountPredicate);
    }

    public static void accumulateBalance(NavigableMap<LocalDate, Map<Account, BigDecimal>> balances) {
        TransactionSummary.accumulateBalance(balances);
    }

    public static Tree<Pair<Account, BigDecimal>> summarizeBalance(Map<Account, BigDecimal> balances, Tree<Account> tree, boolean ignoreZeroBalance) {
        return TransactionSummary.summarizeBalance(balances, tree, ignoreZeroBalance);
    }

    public static Tree<Pair<Account, BigDecimal>> summarizeBalance(Map<Account, BigDecimal> balances, ChartOfAccounts chartOfAccounts, boolean ignoreZeroBalance) {
        return TransactionSummary.summarizeBalance(balances, chartOfAccounts, ignoreZeroBalance);
    }

    public static <T> NavigableMap<LocalDate, Map<T, BigDecimal>> accumulatedBalance(Map<T, List<LedgerEntry>> ledger, NavigableSet<LocalDate> dates) {
        return TransactionSummary.accumulatedBalance(ledger, dates);
    }

    public static void differenceBalance(NavigableMap<LocalDate, Map<Account, BigDecimal>> balances) {
        TransactionSummary.differenceBalance(balances);
    }

    public TransactionSummaryStatistics statistics() {
        return TransactionSummaryStatistics.ofOrderedTransactions(transactions);
    }

    public Stream<Transaction> filterByDate(LocalDate from, LocalDate to) {
        return TransactionSummary.filterByDate(transactions, from, to);
    }

    public Stream<Transaction> filterByDate(String from, String to) {
        return filterByDate(LocalDate.parse(from), LocalDate.parse(to));
    }

    public VirtualAccount virtualAccount(String va) {
        return VirtualAccount.of(va, book.getChartOfAccounts());
    }

    public NavigableMap<LocalDate, Tree<Pair<Account, BigDecimal>>> balanceTrees(NavigableMap<LocalDate, Map<Account, BigDecimal>> balances, boolean ignoreZeroBalances) {
        return TransactionSummary.balanceTrees(balances, book.getChartOfAccounts(), ignoreZeroBalances);
    }

    public <T> Map<T, BigDecimal> balanceByClassifier(Function<TransactionEntry, T> classifier, LocalDate date) {
        if (date != LocalDate.MIN) {
            return TransactionSummary.filterByDate(transactions, LocalDate.MIN, date)
                    .collect(StreamUtils.flatMapping(tx -> tx.getEntries().stream(),
                            Collectors.groupingBy(classifier,
                                    TransactionSummary.amountCollector(TransactionEntry::getAmount))));
        } else {
            return Collections.emptyMap();
        }
    }

    public <T> Map<T, BigDecimal> balanceByMultiClassifier(Function<TransactionEntry, ? extends Iterable< ? extends T>> multiClassifier, LocalDate date) {
        if (date != LocalDate.MIN) {
            return TransactionSummary.filterByDate(transactions, LocalDate.MIN, date)
                    .collect(StreamUtils.flatMapping(tx ->  tx.getEntries().stream(),
                            StreamUtils.multiGroupingBy(multiClassifier,
                                    TransactionSummary.amountCollector(TransactionEntry::getAmount))));
        } else {
            return Collections.emptyMap();
        }
    }

    public Stream<Transaction> journal(LocalDate startDate, LocalDate endDate,
                                       Predicate<TransactionEntry> predicate) {
        Stream<Transaction> result;
        if (startDate == null && endDate == null) {
            result = transactions.stream();
        } else {
            if (startDate == null) startDate = LocalDate.MIN;
            if (endDate == null) endDate = LocalDate.MAX;
            result = TransactionSummary.filterByDate(transactions, startDate, endDate);
        }

        if (predicate != null) {
            result = result.filter(TransactionSummary.transactionPredicateByEntry(predicate));
        }
        return result;
    }

    public Map<?, List<LedgerEntry>> ledger(LocalDate startDate, LocalDate endDate,
                             Predicate<TransactionEntry> filter,
                             Function<TransactionEntry, Object> classifier,
                             Function<TransactionEntry, ? extends Iterable<Object>> multiClassifier) {
        // get transactions
        Stream<Transaction> stream;
        if (startDate == null && endDate == null) {
            stream = transactions.stream();
        } else {
            if (startDate == null) startDate = LocalDate.MIN;
            if (endDate == null) endDate = LocalDate.MAX;
            stream = TransactionSummary.filterByDate(transactions, startDate, endDate);
        }

        if (filter == null && classifier == null && multiClassifier == null) {
            return TransactionSummary.ledger(stream);
        }

        Map<Object, BigDecimal> balance;
        Collector<Pair<Transaction, TransactionEntry>, Map<Object, List<LedgerEntry>>, Map<Object, List<LedgerEntry>>> collector;
        if (multiClassifier != null) {
            balance = balanceByMultiClassifier(multiClassifier, startDate);
            collector = TransactionSummary.ledgerMultiCollector(multiClassifier, key -> balance.getOrDefault(key, BigDecimal.ZERO));
        } else {
            if (classifier == null) classifier = TransactionEntry::getAccount;
            balance = balanceByClassifier(classifier, startDate);
            collector = TransactionSummary.ledgerCollector(classifier, key -> balance.getOrDefault(key, BigDecimal.ZERO));
        }

        return stream.flatMap(tx -> {
            Stream<TransactionEntry> entries = tx.getEntries().stream();
            if (filter != null) {
                entries = entries.filter(filter);
            }
            return entries.map(entry -> ImmutablePair.of(tx, entry));
        }).collect(collector);
    }

    public NavigableMap<LocalDate, Tree<Pair<Account, BigDecimal>>> balanceSheet(
            LocalDate startDate, LocalDate endDate,
            TemporalUnit period, Boolean ignoreZeroBalances) {
        if (startDate == null) startDate = LocalDate.MIN;
        if (endDate == null) endDate = LocalDate.MAX;
        if (ignoreZeroBalances == null) ignoreZeroBalances = Boolean.TRUE;
        if (period == null) period = ChronoUnit.MONTHS;

        final DateSet dates = new DateSet(startDate, endDate, period);
        final NavigableMap<LocalDate, Map<Account, BigDecimal>> balances = TransactionSummary.balance(transactions, dates);
        TransactionSummary.accumulateBalance(balances);
        return TransactionSummary.balanceTrees(balances, book.getChartOfAccounts(), ignoreZeroBalances);
    }

    public NavigableMap<LocalDate, Tree<Pair<Account, BigDecimal>>> incomeStatement(
            LocalDate startDate, LocalDate endDate,
            TemporalUnit period, Boolean ignoreZeroBalances) {
        if (startDate == null) startDate = LocalDate.MIN;
        if (endDate == null) endDate = LocalDate.MAX;
        if (ignoreZeroBalances == null) ignoreZeroBalances = Boolean.TRUE;
        if (period == null) period = ChronoUnit.MONTHS;

        final DateSet dates = new DateSet(startDate, endDate, period);
        final NavigableMap<LocalDate, Map<Account, BigDecimal>> balances = TransactionSummary.balance(transactions, dates, TransactionSummary.incomeAccountPredicate());
        return balanceTrees(balances, ignoreZeroBalances);
    }

    public NavigableMap<LocalDate, Map<Boolean, Map<Account, BigDecimal[]>>> cashFlow(
            LocalDate startDate, LocalDate endDate,
            TemporalUnit period, Predicate<Account> cashAccounts) {
        if (startDate == null) startDate = LocalDate.MIN;
        if (endDate == null) endDate = LocalDate.MAX;
        if (period == null) period = ChronoUnit.MONTHS;

        final DateSet dates = new DateSet(startDate, endDate, period);
        return TransactionSummary.cashFlow(transactions, cashAccounts, dates);
    }

    public Map<Commodity, List<InventoryEntry>> inventory(LocalDate startDate, LocalDate endDate,
                                                          Predicate<Commodity> predicate) {
        if (startDate == null) startDate = LocalDate.MIN;
        if (endDate == null) endDate = LocalDate.MAX;
        final Predicate<Commodity> basePredicate = TransactionSummary.commodityPredicate(book.getDefaultCurrency());
        final Predicate<Commodity> commodityPredicate = predicate == null ? basePredicate : basePredicate.and(predicate);

        if (startDate == LocalDate.MIN) {
            return TransactionSummary.inventory(TransactionSummary.filterByDate(transactions, startDate, endDate),
                    commodityPredicate);
        } else {
            final Map<Commodity, InventoryEntry> balance = TransactionSummary.filterByDate(transactions, LocalDate.MIN, startDate)
                    .flatMap(tx -> tx.getEntries().stream()
                            .filter(TransactionSummary.transactionEntryPredicateByCommodity(commodityPredicate))
                            .map(entry -> ImmutablePair.of(tx, entry)))
                    .collect(Collectors.groupingBy(pair -> pair.second().getCommodity(null),
                            TransactionSummary.inventoryEntryCollector()));

            return TransactionSummary.filterByDate(transactions, startDate, LocalDate.MAX)
                    .flatMap(tx ->
                        tx.getEntries().stream()
                        .filter(TransactionSummary.transactionEntryPredicateByCommodity(commodityPredicate))
                        .map(entry -> ImmutablePair.of(tx, entry)))
                    .collect(TransactionSummary.inventoryCollector(entry -> entry.getCommodity(null),
                            commodity -> {
                                final InventoryEntry entry = balance.get(commodity);
                                return entry == null ? BigDecimal.ZERO : entry.getBalance();
                            },
                            commodity -> {
                                final InventoryEntry entry = balance.get(commodity);
                                return entry == null ? BigDecimal.ZERO : entry.getCountBalance();
                            },
                            commodity -> {
                                final InventoryEntry entry = balance.get(commodity);
                                return entry == null ? Collections.emptyMap() : entry.getCostBalance();
                            }));
        }
    }

    public static void test(Map<String, Object> map) {
        map.entrySet().forEach(e -> {
            System.out.println("key: " + e.getKey());
            System.out.println("value: " + e.getValue());
        });
        final Predicate<Integer> predicate = (Predicate<Integer>) map.get("b");
        System.out.println(predicate.test(3));
        System.out.println(map.getClass());
    }

    public static void main(String[] args) throws ScriptException {
        final ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");
//        final Bindings bindings = engine.getBindings(ScriptContext.ENGINE_SCOPE);
//        bindings.put("",
        final String script = "var T = Java.type('daybook.io.shell.ShellContext');" +
                "T.test({'a': 1, 'b': function(n) {return n%2 === 0;}});";
        engine.eval(script);
    }
}
